docs(agents): correct input capability list in AGENTS.md - #588
Merged
Conversation
src/input/ has keyboard, mouse, and gamepad input sources, not touch. Also audited the rest of the Project Overview list against /src; the other bullets (ECS, rendering, physics, audio, animations, particles, asset loading, FSM) already match the implementation, and no other file duplicates the touch claim. Fixes #582
stormmuller
enabled auto-merge (squash)
August 6, 2026 21:59
The Aug 7 2026 GitHub Actions incident (ARC runner pods stuck idle) dropped this branch's push/pull_request-triggering events, so the required CI workflow never ran on this PR. Empty commit to retrigger it.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
stormmuller
pushed a commit
that referenced
this pull request
Aug 7, 2026
…ng on dev Merges dev, which brings in #587 (the parent-transform fix filed as #581) and #588 (the AGENTS.md input capability correction filed as #582). Both were surfaced by the review of this document, and both are now fixed, so the document's claims about them were stale. The transform gap row and its explanatory note move to past tense. #587 landed exactly the composition the note proposed - scale the child's local offset by the parent's world scale, rotate it by the parent's world rotation, then add - and deleted the three superseded parent-*-system.ts files along with it. The note is retained rather than removed, since the design's assumptions were formed against the broken behavior and it is worth knowing they no longer are. DL-03's "does this survive the fix" subsection moves to past tense too, and now records that option (b) is no longer broken while still losing on the load-bearing arguments. Open question 2 loses its blocker: world-space canvas mode depended on the transform fix, since a health bar parented to a rotating ship was exactly the broken case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw
9 tasks
stormmuller
added a commit
that referenced
this pull request
Aug 8, 2026
* docs(ui): add design document for a uGUI-style UI system Adds /design/ui-system.md, a full architecture and delivery plan for a retained-mode, ECS-native UI system modelled on Unity's uGUI (Canvas / RectTransform / Graphic / EventSystem) rather than IMGUI or UI Toolkit. Covers coordinate spaces, the per-frame system pipeline and its ordering constraints, RectTransform anchor/pivot resolution, the text rendering pipeline, and the pointer interaction state machine, plus a twelve-entry decision log, a six-phase feature backlog, risks, and a testing strategy. Also documents the new /design directory in AGENTS.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): name the model generically and correct the draw-order claim Addresses two review comments on the UI design document. Replaces "uGUI" as the name for Forge's chosen model with the descriptive "anchored rect tree", used consistently throughout. Unity, Godot, and Flash are still cited, but only as prior art in the comparison table and where the text refers specifically to Unity's implementation - Forge no longer borrows another engine's product name as its own vocabulary. The immediate-mode and markup-and-stylesheet alternatives are likewise named by what they are rather than by Unity's product names for them. Corrects the "hierarchy order is draw order" bullet, which overclaimed. Draw order in Forge is a chain - culling mask, camera layer and query order, sprite layer, then depth from world Y - so hierarchy ordering is a property this design must build (via DL-06), not one it inherits, and it holds only within a single canvas and sprite layer. Spells that chain out explicitly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): correct the parent-transform claim and link it to issue #581 The §4.1 table said entity hierarchy "ignores parent rotation/scale", which reads as though neither propagates. That is wrong: composeWithParent inherits rotation additively and scale multiplicatively, both correctly. What is actually broken is narrower - a child's local position offset is composed by plain addition, so it is never rotated or scaled by the parent's world transform, and a child of a rotating parent spins in place instead of orbiting. Restates the gap precisely, adds a note working through the missing terms, a concrete reproduction, and the finding that transform-system.test.ts never exercises a rotated or scaled parent so nothing locks the behaviour in. Also records that the three superseded parent-*-system.ts files carry the same composition but are unexported and referenced only by their own tests. Filed as #581. The design no longer treats this as a constraint to work around; DL-03's rationale is restated so it no longer leans on the bug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): drop touch input from scope and link the AGENTS.md accuracy issue Touch input is out of scope for the UI design. Removes backlog item 0.2 (TouchInputSource), leaving the numbering gap in place so the decision stays visible rather than being silently renumbered. DL-07 now states the consequence explicitly: PointerStateEcsComponent is specified as a source-agnostic pointer written by MouseInputSource today, so a TouchInputSource can later become another writer of the same component without any system above it changing. The stale getBoundingClientRect fix remains a genuine prerequisite and is called out as the only one. The AGENTS.md claim of touch support, which does not exist in /src/input, is tracked in #582 rather than being fixed as a side effect of this design. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): switch draw order to sprite.sortDepth, fix the gap table, scope out clipping Three review points. DL-06 reversed. Draw order now comes from an optional sortDepth on SpriteEcsComponent rather than the dead DepthEcsComponent. layer already lives on the sprite and its JSDoc already documents the world-Y tie-break that sortDepth overrides, so splitting one sort key across two components was arbitrary. It is also free: buildCameraCommands runs per camera and already does three getComponent calls per sprite, and this design adds a second camera - a separate component made that four times sprites times cameras, where a sprite field is a property read on an object already in the batch. A separate component also buys no composition, since a draw-order key is meaningless on an entity with nothing to draw. DepthEcsComponent is now slated for deletion (item 0.11) rather than resurrection. Gap table rebuilt. The severity column mixed urgency, category, and scope ("Blocking", "Opportunity", "Expected", "Bug"), so "Expected" answered nothing. Replaced with one ordered priority scale plus a column naming the backlog item or issue that owns each gap. The rect/rect-transform gap is a Blocker, not a shrug - it is foundational and must be built first. Clipping and masking moved out of scope, tracked in #583. Like text, it is generally useful outside UI - minimaps, wipe transitions, fog-of-war, fill-by-reveal bars - so it belongs in /src/rendering. DL-09 keeps the decision record since the UI design surfaced it, and notes it blocks only ScrollRect (3.4), not Phases 0-2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): move text rendering out of scope, tracked in #584 Text is generally useful outside the UI module - damage numbers, dialogue, floating names, debug overlays - so like clipping it belongs in its own module rather than buried under /src/ui. Filed as #584 covering MSDF atlas loading, the shader, shaping, and a default shipped atlas. The sub-quad expansion refactor of render-system.ts moves with it. It existed to serve text, and it was the highest-risk item in this plan, so the UI critical path no longer carries it. DL-04 and DL-05 stay as decision records with scope banners pointing at #584. Phase 0 is rebuilt and renumbered: what remains is the canvas-space pointer, Rect2, sprite.sortDepth, deleting DepthEcsComponent, and verifying UI-camera compositing. A table records what moved out and where, since three separate removals had left the numbering full of holes. States the dependency plainly rather than hiding it: every phase can be built and unit-tested without text, but the module is not useful without it, so text landing should gate the UI module's first release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): move interaction events onto the interactable, drop ButtonEcsComponent Adds DL-13, covering two related review points. Buttons are not the only clickable things. Toggles, sliders, scrollbar thumbs, list rows, inventory slots, cards and close icons all want onClick and the pointer enter/exit pair, so hanging the event surface off ButtonEcsComponent forced each of them to claim to be a button or duplicate the events. Events now live on UiInteractableEcsComponent - the component that already means "this rect participates in pointer input". That leaves nothing for a button component to hold: visual feedback belongs to a transition component and focus to UiFocusEcsComponent. A button is fully described by interactable + transitions + a child label, so it becomes createButton, an aggregate factory following the existing createCamera precedent. UiPointerStateEcsComponent is merged in as well. The State suffix was the tell - every component is state, and nothing ever wanted the interaction state without the interactable. The codebase already keeps author-set and system-written fields together where they belong: PositionEcsComponent holds local beside world. Input's pointer component is renamed PointerEcsComponent for the same reason. Also records the sprite pivot Y-axis inconsistency as issue #585 rather than documenting it as a permanent gotcha; once fixed, the compensating `1 - pivot.y` bridge in the layout system should be deleted, not kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): specify same-tick pointer transitions so Hovered can be skipped The interaction state machine had no path from Normal to Pressed, which implied a press could not be observed on the same tick the pointer entered. That is wrong twice over. Input is sampled per tick, not streamed: MouseInputSource accumulates button downs and ups into per-frame sets and resets them each tick, so a system sees the current position plus a set of edges, never their intra-frame ordering. A flick-and-click fits inside one 16.7ms frame, and touch has no hover phase at all - its first event is simultaneously entered and down. Since DL-07 specifies the pointer source-agnostically so touch can be added later without revisiting the design, the state machine has to tolerate a missing hover for that promise to hold. The related case is press and release within one tick, from a synthetic or fast enough click. Both edges survive the frame in separate sets, but a system reading only "is the button held" sees neither and drops the click. Specifies the system as deriving state from each tick's facts and events from the delta against the previous tick, rather than applying one transition per tick, and adds the Normal to Pressed edge so that path is legal rather than accidental. Callers are told to treat the states as sampled, not as a guaranteed sequence. Both same-tick cases are added to the unit test plan, since they are cheap to assert synthetically and nearly impossible to reproduce by hand. Also fixes a duplicate const in the polled/evented example. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): make activation source-agnostic via InputAction, not a pointer click Adds DL-14. The interaction model was pointer-centric - onClick, raised only by a mouse - with gamepad navigation parked in Phase 5 polish. A controller has no cursor, no hover and no click; it has a focused element and a submit action, so that shape either excluded controllers or bolted them on as a parallel path every consumer handles twice. The engine already solved this a layer down and this design should not re-solve it differently. /src/input decouples InputAction from InputSource so game code says "jump was actioned" rather than "space was pressed", and CameraEcsComponent already consumes that directly via zoomInput/panInput rather than reading keys. The UI canvas now takes submitInput/cancelInput/navigateInput the same way and never touches an input source. onClick becomes onActivate, raised from either the pointer path or the focus path, with the source never exposed. isHovered stays pointer-only; isFocused is source-agnostic; transitions key off a derived state merging both so an element highlights the same whether moused-over or stick-focused. Drag events stay explicitly pointer-shaped, since a drag has no controller analogue. UiFocusEcsComponent and the navigation system move from Phase 5 to Phase 2 core. This also makes the UI programmatically drivable for free: because activation arrives through an InputAction, a test or scripted tutorial calls action.trigger() and the UI responds exactly as it would to a player - no synthesized DOM events and no test-only code path. Separately, addresses the DX concern that an author should not assemble cameras and canvases to show a button. Adds a "minimal path" showing the two calls that put a working, gamepad-navigable button on screen, and states that every option is defaulted and the second camera is an implementation detail of createUiCanvas. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): document post-processing scope as a choice, not a constraint DL-01 presented a separate UI render target as giving "the right post-processing semantics by default", which read as though UI could never be included in a full-screen effect. That is wrong, and post-processing over UI is a legitimate thing to want. Post-processing in this engine attaches to a render target, not to a camera: bloom-system dedupes with processedTargetsThisFrame.has(renderTarget), so an effect is applied once per target regardless of how many cameras drew into it. That single fact gives all three scopes with no new plumbing - world-only via separate targets (the default), everything via pointing the UI camera at the world camera's target, and UI-only via attaching effects to the UI camera. Ordering already works for the shared case, since post-processing systems register after the render system and before the present system. Also records the one real constraint: a shared target means one set of effects over both, so bloom-the-world-but-not-the-UI combined with tone-map-everything needs two passes. Notes that as the argument for eventually attaching a post-processing stack to the present step rather than only to a camera, and marks it out of scope here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): fix stale field name in DL-13 after the DL-14 rename DL-13's consequences still listed wasClickedThisFrame, which DL-14 renamed to wasActivatedThisFrame, and omitted isFocused entirely. Also notes that isFocused and wasActivatedThisFrame are written by the navigation system as well as the interaction system, so neither has a single owning system. Found by a consistency sweep over the document rather than by CI, which has not run on this branch since 031d13b. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): state whether DL-03 survives the transform fix, and correct the argument Answers directly whether the canvas-scaling decision changes once #581 lands. It does not, but the original rationale leaned on "(b) is broken" as its headline argument, which was the one objection with an expiry date. Adds a per-channel table making the composition rules explicit, since they differ and only one is wrong: rotation composes additively (correct - nesting rotations adds angles), scale composes multiplicatively (correct - a 2x parent with a 3x child is 6x), and position composes additively but omits transforming the local offset by the parent's scale and rotation first. So #581 is not "make scale and rotation additive like position"; those are already right. Records that once #581 lands, option (b) stops being broken - a scaled canvas root would scale children's offsets, and sprite sizes already scale via bindSpriteInstanceData - but still loses on the load-bearing arguments: it dirties every UI entity's world transform on resize where (a) changes one camera field, it puts rects in a screen-dependent space instead of reference pixels, it invalidates the isStatic freeze from DL-12 on every resize, and it makes a per-element press animation scale against a screen-dependent base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): record how the two-camera setup constrains DL-07's pointer The camera separation described in section 3 does affect DL-07, and the decision did not say so. Because UI renders through its own camera, one canvas-space pointer maps to two different world positions - one through each camera - and they diverge as soon as the world camera pans or zooms. So PointerEcsComponent publishes canvas pixels only and stays camera-agnostic as well as source-agnostic. It must not publish a world position, because there is no single correct one. Conversion is the caller's job and screenToWorldSpace is already camera-parameterized, so the UI raycaster converts through the UI camera and world picking converts through the world camera, with neither privileged. Also records the consistency requirement in the other direction: the raycaster must only hit-test what the UI camera can see, since an element culled by cullingMask that remains clickable produces invisible hit regions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): reconcile text input staying in UI while text rendering moves out Answers why a text input belongs in this module when text rendering does not. They look inconsistent but aren't: a text input is three separable concerns. Text entry (keystrokes, caret, IME, clipboard, soft keyboard) is an input concern and almost always implies a focused widget, unlike damage numbers or world signage. Text display is identical to drawing any other text and belongs to #584. The focusable rect it hangs off is exactly what this module is. Fixes a real gap in the backlog bookkeeping while there: item 3.4 was marked blocked on #583 but 3.5 was not marked blocked on #584, despite being far more blocked. A scroll view merely looks wrong without clipping; a text field is unusable without text rendering, since you cannot see what you type. Also splits the hidden-input bridge out into /src/input, for the same reason DL-07 moved the pointer there. That bridge is where the IME and mobile-keyboard complexity lives and is reusable by anything needing text entry rather than being specific to one widget. As a result the "no DOM-backed widgets" non-goal becomes a real invariant instead of one with an exception carved out of it - the UI module now stays DOM-free. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): split text input out into its own issue (#586) Text input follows text rendering and clipping out of the UI module. It is the strongest candidate of the three: it was the most hard-blocked item in the backlog (unusable, not merely imperfect, without #584), and its genuinely hard part - IME composition, mobile soft keyboards, clipboard, selection, accessibility - is an input concern rather than a UI one. #586 covers a text-entry primitive in /src/input wrapping a hidden DOM input, plus the thin UI component that consumes it. DL-10 stays as the decision record with a scope banner, and item 3.5 is marked out of scope. Also flags something for #584 before its API is fixed: rendering a caret and selection highlight needs per-glyph x-positions, so TextMeshEcsComponent should expose glyph advances rather than only quads. Retrofitting that later is more disruptive than including it from the start. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): replace Rect in place rather than adding a parallel Rect2 Per review, the plain-object rect keeps the name Rect and replaces the existing class outright, rather than coexisting with it under a new name. Not documented as a breaking change. The blast radius turns out to be genuinely small, which supports replacing rather than deprecating: Rect is constructed nowhere in /src outside its own tests, and its only consumer is CameraEcsComponent.scissorRect, whose type name does not change. What changes is construction and the two methods becoming static calls. Mirrors vector2.ts exactly - a plain interface for the data, a static-only class for the operations with mutating helpers taking a target first argument. Keeps { min, max } over the current { origin, size }, since anchor math reads min/max far more often and size is one subtraction away. Folds in a file rename to rect.ts, since src/math/Rect.ts violates the kebab-case convention in AGENTS.md and a full rewrite is the cheapest moment to fix it. Leaves one naming question open in section 11: Vector2/Vec2 puts the long name on the type and the short one on the namespace, which does not translate since Rect has no natural abbreviation. Rects is proposed; Rectangle/Rect is the more literal mirror. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw * docs(ui): update for the transform fix and AGENTS.md correction landing on dev Merges dev, which brings in #587 (the parent-transform fix filed as #581) and #588 (the AGENTS.md input capability correction filed as #582). Both were surfaced by the review of this document, and both are now fixed, so the document's claims about them were stale. The transform gap row and its explanatory note move to past tense. #587 landed exactly the composition the note proposed - scale the child's local offset by the parent's world scale, rotate it by the parent's world rotation, then add - and deleted the three superseded parent-*-system.ts files along with it. The note is retained rather than removed, since the design's assumptions were formed against the broken behavior and it is worth knowing they no longer are. DL-03's "does this survive the fix" subsection moves to past tense too, and now records that option (b) is no longer broken while still losing on the load-bearing arguments. Open question 2 loses its blocker: world-space canvas mode depended on the transform fix, since a health bar parented to a rotating ship was exactly the broken case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AGENTS.md's Project Overview listed the engine's input capabilities as "Keyboard, mouse, and touch input handling".src/input/has keyboard, mouse, and gamepad input sources — there is noTouchInputSource. Corrected the line to say "Keyboard, mouse, and gamepad input handling".While in there, audited the rest of the Project Overview list against
/src: ECS, rendering (WebGL2), physics (rigid bodies, collision, gravity), audio (Howler.js), animations, particles, asset loading, and FSM all match their implementations — no further drift found. Also checkedREADME.mdanddocumentation-site/docsfor the same "touch" claim; neither duplicates it, so no other file needed a change.This is documentation-only; no
TouchInputSourcewas added (that's explicitly out of scope per the issue).Related issue(s)
Closes #582
Verification checklist
npm run check-typespasses with 0 errors (no/srcchanges made)npm testpasses (no/srcchanges made)npm run lintpasses with 0 errorsnpm run cspellpasses with 0 errorsnpm run check-exportspasses (no/srcchanges made)index.ts— N/A, no API change/documentation-site/docs/docsis updated if this change affects documented behavior — N/A, no documented behavior changed,AGENTS.mditself is the doc being corrected/srcmodule touchedChangelog
docsGenerated by Claude Code